﻿using System;
using System.Collections.Generic;
using System.Text;
using CodePlex.Resourceful;
using CodePlex.Resourceful.Microsoft.SSDS;
using CodePlex.Resourceful.Amazon.S3;
using System.Text.RegularExpressions;

namespace CodePlex.SpaceBlock.FS
{
    public class SSDSFileSystem : BaseFileSystem, IFileSystem, ISupportsTransferWithMetadata
    {
        private readonly SSDSConnection _connection;
        private readonly string _authority;

        public SSDSFileSystem(SSDSConnection connection, string authority)
        {
            if (string.IsNullOrEmpty(authority))
            {
                throw new ArgumentException("Authority cannot be blank.", "authority");
            }

            // When creating an authority, the Authority Id must contain only lowercase letters, numbers, or dashes.
            if (!Regex.IsMatch(authority, @"^[a-z0-9\-]+$") || authority.StartsWith("-") || authority.EndsWith("-"))
            {
                throw new ArgumentException("The Authority must contain only lowercase letters, numbers, or dashes, and cannot start or end with a dash.", "authority");
            }

            _connection = connection;
            _authority = authority;
        }

        public Tracer Tracer
        {
            get { return _connection.Tracer; }
        }

        public SSDSConnection SSDSConnection
        {
            get
            {
                return _connection;
            }
        }


        private bool? _isValidated;

        private void EnsureValid()
        {
            if (_isValidated == null)
            {

                Operation getAuth = delegate
                {
                    Authority authority = _connection.GetAuthority(_authority);

                    if (authority == null)
                    {
                        throw CodePlexException.Format("Authority '{0}' not found.", _authority);
                    }
                };

                try
                {
                    getAuth();
                }
                catch 
                {
                    _connection.CreateAuthority(_authority);

                    getAuth();
                }

                _isValidated = true;
            }
        }

        public string Caption
        {
            get
            {
                this.EnsureValid(); // TODO expected side effect = bad
                return string.Format("SSDS File System - {0}", this.ShortCaption);
            }
        }

        public string ShortCaption
        {
            get
            {
                return _connection.Username + "@" + _authority + (_connection.IsSSL ? " (ssl)" : "");
            }
        }










        // implementation
        /*
        File
          ID = "SpaceBlockFile" + FilePath (/ -> :)
          Kind = "SpaceBlockFile"
          ParentFolderPath
          content-length  *
          Content-disposition   *
          content-type *
          LastModified 
 

        File Contents
          ID = "SpaceBlockBlob" + FilePath (/ -> :)
          content-length  *
          Content-disposition   *
          content-type *

        Folder
          ID = "SpaceBlockFolder" + FolderPath  (/ -> :)
          Kind = "SpaceBlockFolder"
          ParentFolderPath
        */

        public ReadOnlyList<IFolderItem> GetFolders(Path path)
        {
            this.CheckNull(path);
            this.CheckFolder(path);


            List<IFolderItem> folders = new List<IFolderItem>();


            if (path.Level == 0)
            {
                // list containers
                foreach (Container c in _connection.GetContainers(_authority))
                {
                    folders.Add(new FolderItem(this, path.Combine(c.ID + Path.SeparatorChar), null));

                }

            }

            else
            {
                SSDSPathInfo info = new SSDSPathInfo(path);

                IEnumerable<Entity> entities = _connection.GetEntities(_authority, info.Container,
                  SSDSE.And(
                      SSDSE.Eq(SSDSE.Kind, "SpaceBlockFolder"),
                      SSDSE.Eq(SSDSE.Property<string>("ParentFolderPath"), info.ContainerLocalPath.ToString())
                  ));

                foreach (Entity entity in entities)
                {
                    folders.Add(ParseSpaceBlockFolder(info.Container,entity));

                }


            }

            return new ReadOnlyList<IFolderItem>(folders);


        }

        public ReadOnlyList<IFileItem> GetFiles(Path path)
        {
            this.CheckNull(path);
            this.CheckFolder(path);

            List<IFileItem> files = new List<IFileItem>();


            if (path.Level == 0)
            {
                return new ReadOnlyList<IFileItem>(files);
            }

            SSDSPathInfo info = new SSDSPathInfo(path);


            IEnumerable<Entity> entities = _connection.GetEntities(_authority, info.Container,
                SSDSE.And(
                    SSDSE.Eq(SSDSE.Kind, "SpaceBlockFile"),
                    SSDSE.Eq(SSDSE.Property<string>("ParentFolderPath"), info.ContainerLocalPath.ToString())
                ));

            foreach (Entity entity in entities)
            {
                SSDSFileItem fileItem = ParseSpaceBlockFile(info.Container, entity);
                files.Add(fileItem);
                
            }

            return new ReadOnlyList<IFileItem>(files);
        }

        private IFolderItem ParseSpaceBlockFolder(string container, Entity entity)
        {
            string folderPathString = entity.ID.Substring("SpaceBlockFolder".Length).Replace(':', '/');
            Path folderPath = Path.Parse(Path.SeparatorChar + container + folderPathString);

            return new FolderItem(this, folderPath, null);
        }

        private SSDSFileItem ParseSpaceBlockFile(string container, Entity entity)
        {
            string filePathString = entity.ID.Substring("SpaceBlockFile".Length).Replace(':', '/');
            Path filePath = Path.Parse(Path.SeparatorChar + container + filePathString);

            DateTime? lastModified = GetDateTime(entity, HttpHeader.LastModified);
            decimal? contentLength = GetDecimal(entity, HttpHeader.ContentLength);

            return new SSDSFileItem(this, filePath, lastModified, (long)contentLength.Value);

        }


        private DateTime? GetDateTime(Entity entity, string propertyName)
        {
            FlexibleProperty fp = Utils.Find(entity.Properties, delegate(FlexibleProperty item)
            {
                return item.Name == propertyName;
            });
            if (fp == null)
            {
                return null;
            }
            return (DateTime)fp.Value;
        }
        private decimal? GetDecimal(Entity entity, string propertyName)
        {
            FlexibleProperty fp = Utils.Find(entity.Properties, delegate(FlexibleProperty item)
            {
                return item.Name == propertyName;
            });
            if (fp == null)
            {
                return null;
            }
            return (decimal)fp.Value;
        }


        private class SSDSFileItem : PathItem , IFileItem
        {
            private readonly long _size;

            public SSDSFileItem(IFileSystem fileSystem, Path path, DateTime? lastModified, long size)
                : base(fileSystem,path,lastModified)
            {
                _size = size;
            }

            public long Size
            {
                get { return _size; }
            }

            public System.IO.Stream GetInputStream()
            {
                if (_size == 0)
                {
                    return new System.IO.MemoryStream();
                }
                SSDSFileSystem fs = this.FileSystem as SSDSFileSystem;

                SSDSPathInfo info = new SSDSPathInfo(this.Path);

                return fs.SSDSConnection.GetEntityBlobAsStream(fs._authority, info.Container, info.GetBlobEntityID());


            }

            
        }


        public ReadOnlyList<IPathItem> GetItems(Path path)
        {
            List<IPathItem> items = new List<IPathItem>();

            foreach(IFolderItem folder in this.GetFolders(path)) 
            {
                items.Add(folder);
            }
            foreach (IFileItem file in this.GetFiles(path))
            {
                items.Add(file);
            }

            return new ReadOnlyList<IPathItem>(items);
           
        }

      

        public bool Exists(Path path)
        {
            this.CheckNull(path);

            if (path.Level == 0)
            {
                return true;
            }

            SSDSPathInfo info = new SSDSPathInfo(path);

            if (path.Level == 1)
            {
                return _connection.ContainerExists(_authority,info.Container);
            }

            return _connection.EntityExists(_authority, info.Container,

                path.IsFolder ? info.GetFolderEntityID() : info.GetFileEntityID()
               );
        }

       

      

      

        public IPathItem GetItem(Path path)
        {
            this.CheckNull(path);



            SSDSPathInfo info = new SSDSPathInfo(path);



            if (path.IsFolder)
            {
                Entity entity = _connection.GetEntity(_authority, info.Container, info.GetFolderEntityID());

                return ParseSpaceBlockFolder(info.Container, entity);
            } 
            else
            {
                Entity entity = _connection.GetEntity(_authority, info.Container, info.GetFileEntityID());

                return ParseSpaceBlockFile(info.Container, entity);
            }

        }

        public void EnsureFolderExists(Path path)
        {
            this.CheckNull(path);
            this.CheckFolder(path);

            if (path.Level == 0)
            {
                return;
            }


            SSDSPathInfo info = new SSDSPathInfo(path);

            if (!_connection.ContainerExists(_authority, info.Container))
            {
                _connection.CreateContainer(_authority, info.Container);
            }

            if (path.Level == 1)
            {
                return;
            }

            if (_connection.EntityExists(_authority, info.Container, info.GetFolderEntityID()))
            {
                return;
            }


            // create spaceblockfolder
            if (path.Level > 2)
            {
                this.EnsureFolderExists(path.Parent);
            }

            string entityID = info.GetFolderEntityID();
            string kind = "SpaceBlockFolder";
            string parentFolderPath = info.ContainerLocalPath.Parent.ToString();

            List<FlexibleProperty> properties = new List<FlexibleProperty>();
            properties.Add(FlexibleProperty.String("ParentFolderPath", parentFolderPath));

            if (_connection.EntityExists(_authority, info.Container, entityID))
            {
                _connection.UpdateEntity(_authority, info.Container, entityID, kind, properties.ToArray());
            }
            else
            {
                _connection.CreateEntity(_authority, info.Container, entityID, kind, properties.ToArray());
            }
            


        }

        public void EnsureFileExists(Path path)
        {
            this.CheckNull(path);
            this.CheckFile(path);

            this.Transfer(new NullFileItem(),path,null);
            
        }

        private class NullFileItem : IFileItem
        {

            public long Size
            {
                get { return 0L; }
            }

            public System.IO.Stream GetInputStream()
            {
                return new System.IO.MemoryStream();
            }

            public IFileSystem FileSystem
            {
                get { throw new NotImplementedException(); }
            }

            public Path Path
            {
                get { throw new NotImplementedException(); }
            }

            public DateTime? LastModified
            {
                get { throw new NotImplementedException(); }
            }

           
        }

        public void Delete(Path path)
        {
            this.CheckNull(path);

            SSDSPathInfo info = new SSDSPathInfo(path);

            if (path.IsFolder)
            {
                if (path.Level == 0)
                {
                    throw CodePlexException.Format("Cannot delete root folder.");
                }

                if (path.Level == 1)
                {
                    _connection.DeleteContainer(_authority, info.Container);
                }
                else
                {
                    foreach (IFileItem file in this.GetFiles(path))
                    {
                        this.Delete(file.Path);
                    }
                    foreach (IFolderItem folder in this.GetFolders(path))
                    {
                        this.Delete(folder.Path);
                    }

                    _connection.DeleteEntity(_authority, info.Container, info.GetFolderEntityID());
                }

            }
            else
            {
                _connection.DeleteEntityIfExists(_authority, info.Container, info.GetFileEntityID());
                _connection.DeleteEntityIfExists(_authority, info.Container, info.GetBlobEntityID());
               
            }
        }

  



        public Metadata GetMetadata(Path path)
        {
            Metadata rt = new Metadata();

            this.AddMetadata(rt, BaseMetadataKeys.Path, path.ToString());
            this.AddMetadata(rt, SSDSMD.Authority, _authority);
            if (path.Level == 0)
            {
                this.AddMetadata(rt, BaseMetadataKeys.Type, "File System");
                this.AddMetadata(rt, SSDSMD.Username, this.SSDSConnection.Username);
                return rt;
            }

            SSDSPathInfo info = new SSDSPathInfo(path);

            this.AddMetadata(rt, SSDSMD.Container, info.Container);

            if (path.Level == 1)
            {
                this.AddMetadata(rt, BaseMetadataKeys.Type, "Virtual Folder");
                return rt;

            }


            if (path.IsFolder)
            {
                this.AddMetadata(rt, BaseMetadataKeys.Type, "Virtual Folder");
                this.AddMetadata(rt, SSDSMD.EntityID, info.GetFolderEntityID());
                 this.AddMetadata(rt, SSDSMD.Kind, "SpaceBlockFolder");
            }
            else
            {
                this.AddMetadata(rt, BaseMetadataKeys.Type, "Virtual File");
                this.AddMetadata(rt, SSDSMD.EntityID, info.GetFileEntityID());
              


                Entity entity = _connection.GetEntity(_authority, info.Container, info.GetFileEntityID());

                this.AddMetadata(rt, SSDSMD.Kind, entity.Kind);
                this.AddMetadata(rt, SSDSMD.Version, entity.Version.ToString());


                foreach (FlexibleProperty fp in entity.Properties)
                {
                    string value = Convert.ToString(fp.Value);

                    if (fp.Name == "ParentFolderPath")
                    {
                        continue;
                    }
                    if (fp.Name == HttpHeader.ContentLength)
                    {
                        value = Utils.ComputeSizeString(Convert.ToInt64(fp.Value));
                    }
                    
                    this.AddMetadata(rt, fp.Name, value);
                }



            }

            return rt;

        }

        private static class SSDSMD
        {
            public static readonly string Username = "Username";
            public static readonly string Authority = "(Authority)";
            public static readonly string Container = "(Container)";
            public static readonly string EntityID = "(EntityID)";
            public static readonly string Kind = "(Kind)";
            public static readonly string Version = "Version";
         
        }

        override protected string GetMetadataCategory(string key)
        {

            if (Utils.In(key,
                SSDSMD.Username,
                SSDSMD.Authority,
                SSDSMD.Container,
                SSDSMD.EntityID,
                SSDSMD.Kind,
                SSDSMD.Version,

                HttpHeader.ContentLength,
                HttpHeader.ContentType,
                HttpHeader.LastModified,
                S3Constants.Headers.ContentDisposition



                ))
            {
                return "SSDS";
            }



            return base.GetMetadataCategory(key);
        }

        override protected string GetMetadataDescription(string key, string category)
        {

            NameValuePairs descriptions = new NameValuePairs();

            descriptions[SSDSMD.Username] = "The username used to access the SSDS beta";
            descriptions[SSDSMD.Authority] = "At the top level of this containment hierarchy is an authority. After you sign up for the data service, you begin by creating an authority. An authority is represented by a DNS name.";
            descriptions[SSDSMD.Container] = "A container stores data (entities). Because each authority is in one geo-location, all containers in an authority are located in the same data center.";
            descriptions[SSDSMD.EntityID] = "An entity is the smallest object that can be updated, that is, you can retrieve an entire entity; add, update, delete properties; and then replace the original entity with the updated one.";
            descriptions[SSDSMD.Kind] = "The Kind property value is user-defined and identifies the entity type.";
            descriptions[SSDSMD.Version] = "The Version property acts like a timestamp.";

            return descriptions.GetIfExists(key) ?? base.GetMetadataDescription(key, category);
        }





      
        public void Transfer(IFileItem source, Path destination, Action<long> bytesTransferred)
        {
            this.Transfer(source, destination, null, bytesTransferred);
        }

        public void Transfer(IFileItem source, Path destination, NameValuePairs metadata, Action<long> bytesTransferred)
        {
            metadata = metadata ?? new NameValuePairs();

            this.CheckNull(destination);
            this.CheckFile(destination);

            this.EnsureFolderExists(destination.Parent);

            SSDSPathInfo info = new SSDSPathInfo(destination);
           

            // write blob
            string blobEntityID = info.GetBlobEntityID();
            string contentType = metadata.GetIfExists(HttpHeader.ContentType) ?? MediaType.ApplicationOctetStream;
            long contentLength = source.Size;
            string contentDisposition = metadata.GetIfExists(S3Constants.Headers.ContentDisposition);

            if (contentLength > 0)  // SSDS does not handle 0-byte files
            {
                System.IO.Stream inputStream = source.GetInputStream();

                if (_connection.EntityExists(_authority, info.Container, blobEntityID))
                {
                    _connection.UpdateEntityBlob(_authority, info.Container, blobEntityID, contentType, contentLength, contentDisposition, inputStream, bytesTransferred);
                }
                else
                {
                    _connection.CreateEntityBlob(_authority, info.Container, blobEntityID, contentType, contentLength, contentDisposition, inputStream, bytesTransferred);
                }
            }
          

            // write file
            string fileEntityID = info.GetFileEntityID();
            string kind = "SpaceBlockFile";
            DateTime lastModified = DateTime.Now;
            string parentFolderPath = info.ContainerLocalPath.Parent.ToString();

            List<FlexibleProperty> properties = new List<FlexibleProperty>();
            properties.Add(FlexibleProperty.String(HttpHeader.ContentType, contentType));
            properties.Add(FlexibleProperty.Decimal(HttpHeader.ContentLength, contentLength));
            properties.Add(FlexibleProperty.DateTime(HttpHeader.LastModified, lastModified));
            properties.Add(FlexibleProperty.String("ParentFolderPath", parentFolderPath));

            foreach (KeyValuePair<string, string> kvp in metadata)
            {
                if (kvp.Key == HttpHeader.ContentType)
                {
                    continue;
                }
                properties.Add(FlexibleProperty.String(kvp.Key, kvp.Value));

            }



            if (_connection.EntityExists(_authority, info.Container, fileEntityID))
            {
                _connection.UpdateEntity(_authority, info.Container, fileEntityID, kind, properties.ToArray());
            }
            else
            {
                _connection.CreateEntity(_authority, info.Container, fileEntityID, kind, properties.ToArray());
            }

           
        }


















        private class SSDSPathInfo
        {
            public readonly string Container;
            public readonly Path ContainerLocalPath;



            public SSDSPathInfo(Path path)
            {
                this.Container = path.Parts[0];

                string localPathString = path.ToString().Substring(this.Container.Length + 1);

                this.ContainerLocalPath = Path.Parse(localPathString);
            }

            public string GetFileEntityID()
            {
                return "SpaceBlockFile" + ContainerLocalPath.ToString().Replace(Path.SeparatorChar, ':');
            }
            public string GetFolderEntityID()
            {
                return "SpaceBlockFolder" + ContainerLocalPath.ToString().Replace(Path.SeparatorChar, ':');
            }

            public string GetBlobEntityID()
            {
                return "SpaceBlockBlob" + ContainerLocalPath.ToString().Replace(Path.SeparatorChar, ':');
            }
        }



    }
}
